﻿/// Copyright © 2013 Microsoft Corporation. 
/// This project is released under the Microsoft Public License (MS-PL)
///

namespace Azure.Performance.Storage
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Threading.Tasks;

    using Microsoft.WindowsAzure.Storage.Auth;
    using Microsoft.WindowsAzure.Storage.Blob;

    using CloudStorageAccount = Microsoft.WindowsAzure.Storage.CloudStorageAccount;

    public class UploadPerformanceTestSdk20WritePages : IUploadPerformanceTest
    {
        private CloudBlobClient blobClient;

        private CloudPageBlob pageBlob;

        private StorageCredentials credentials;

        public void Dispose()
        {
            this.Clean();
        }

        public void Clean()
        {
            if (null != pageBlob)
                pageBlob.DeleteIfExists();
        }

        public string Id
        {
            get
            {
                return "BA94D612-6A5F-41A8-ABD7-D073FFB9DB27";
            }
        }

        public string Name
        {
            get
            {
                return "Azure Storage SDK 2.0 Upload Using WritePages()";
            }
        }

        public string Description
        {
            get
            {
                return "Use Storage 2.0 Client Libraries to upload a Page BLOB using WritePages() approach similar to previous SDK";
            }
        }

        [Setting(Name = "Threads", Description = "Number of upload threads to use")]
        public int Threads { get; set; }

        [Setting(Name = "FileSize", Description = "Size of the test file to upload")]
        public long FileSize { get; set; }

        [Setting(Name = "SSL", Description = "Use an SSL connection with the upload")]
        public bool UseSsl { get; set; }

        public void Initialize(string storageAccount)
        {
            CloudStorageAccount account =
                CloudStorageAccount.Parse(storageAccount);

            this.credentials = account.Credentials;

            blobClient = account.CreateCloudBlobClient();
            blobClient.ParallelOperationThreadCount = Threads;

            var uploadContainer = blobClient.GetContainerReference("upload");
            uploadContainer.CreateIfNotExists();


            pageBlob = uploadContainer.GetPageBlobReference(Guid.NewGuid().ToString().ToUpper().Replace("-", "") + ".bin");

        }

        // Return a simple list of 4MB offsets
        private IEnumerable<long> GetPages(string file)
        {
            var pages = new List<long>();

            var finfo = new FileInfo(file);
            var size = finfo.Length;

            var fullpages = size / (4 * 1024 * 1024);

            var i = 0;
            for (; i < fullpages; i++)
                pages.Add(i * 4 * 1024 * 1024);

            if ((size % (4 * 1024 * 1024)) > 0)
                pages.Add(i * 4 * 1024 * 1024);

            return pages;
        }

        //TODO: We can clean this up a bit and further optimize a few things here.
        private void UploadPageRange(string file, long fileOffset)
        {
            using (var fs = File.OpenRead(file))
            {

                var reader = new BinaryReader(fs);
                fs.Seek(fileOffset, SeekOrigin.Begin);

                int offsetToTransfer = -1;

                //TODO: Move some of these magic numbers to constants and general cleaning
                var range = reader.ReadBytes(4 * 1024 * 1024);

                var offsetInRange = 0;

                // Make sure end is page size aligned
                if ((range.Length % 512) > 0)
                {
                    var grow = (512 - (range.Length % 512));
                    Array.Resize(ref range, range.Length + grow);
                }

                // Upload groups of contiguous non-zero page blob pages.
                while (offsetInRange <= range.Length)
                {
                    if ((offsetInRange == range.Length) ||
                        IsAllZero(range, offsetInRange, 512))
                    {
                        if (offsetToTransfer != -1)
                        {
                            // Transfer up to this point
                            int sizeToTransfer = offsetInRange - offsetToTransfer;
                            var memoryStream = new MemoryStream(range,
                                            offsetToTransfer, sizeToTransfer, false, false);

                            var blob = new CloudPageBlob(pageBlob.Uri, credentials);

                            blob.WritePages(memoryStream, fileOffset + offsetToTransfer);

                            offsetToTransfer = -1;
                        }
                    }
                    else
                    {
                        if (offsetToTransfer == -1)
                        {
                            offsetToTransfer = offsetInRange;
                        }
                    }
                    offsetInRange += 512;
                }
            }
        }

        //TODO: Change this to test with explicitly control number of threads instead of simply setting MAX
        public void Upload(string file)
        {
            var finfo = new FileInfo(file);
            var size = finfo.Length;

            pageBlob.Create((size + 512 - 1) & ~(512 - 1));

            var pages = this.GetPages(file);

            var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = this.Threads };

            Parallel.ForEach(
                pages,
                parallelOptions,
                page => this.UploadPageRange(file, page));
        }

        private static bool IsAllZero(byte[] range, long rangeOffset, long size)
        {
            for (long offset = 0; offset < size; offset++)
            {
                if (range[rangeOffset + offset] != 0)
                {
                    return false;
                }
            }
            return true;
        }
    }
}
